
'''
Created on Aug 25, 2011

@author: david
'''
# python lib support
import os.path
import logging
from glob import glob

# pspy support
from pspy import CONTENT_TYPES, Notification
from pspy.gae.web import Request, Response

# 3rd party (local)
from genshi.template import TemplateLoader 

DEBUG_FLAG = 0

# HTTP Constants 
HTTP_CODE_OK = 200
HTTP_CODE_SERVER_ERROR = 500
HTTP_CODE_NOT_FOUND = 400
HTTP_MESSAGES = { HTTP_CODE_OK:"OK", HTTP_CODE_NOT_FOUND:"URI Not Found", HTTP_CODE_SERVER_ERROR:"Internal Server Error (See Logs)"}
HTTP_RETURN_JSON = "application/json"
HTTP_RETURN_HTML = "text/html"
HTTP_RETURN_XML = "text/xml"
HTTP_RETURN_JAVASCRIPT = "text/javascript"
HTTP_RETURN_TEXT = "text/plain"
HTTP_HEADERS_TMPLT= "Content-type: %s\nContent-length: %s"

# local constants
TEMPLATE_DIR = "./web"
DEFAULT_ERROR_PAGE = '/site_error.gen' 

# static file types to server directly
STATICS = ["html","xhtml","xml","txt","js","css","pdf","jpeg","jpg","gif","png","json","mp3","mp4","mov"]

##
# Module exception classes
class ActionDomainError(RuntimeError):
    "module exception class"

class AuthenticationError(Exception):
    "login exception class"

class ActionError(RuntimeError):
    "Action oriented exception"

class SessionError(Exception):
    "Session related exception"

##
# Base class for action classes in pspyder.
# Not actually required as a base class
# (as long as the proper interface is
# implemented), this class is a nice 
# utility base class where any and all
# special pspy gear can be installed and
# accessed by the whole family.
class PspyAction(object):    
    # class level attributes
    CHECK_OK = True
    CHECK_FAILED = False

    "base implementation"
    def __init__(self, req=None):
        self._request = req
    
    # create request property
    def _getReq(self):
        return self._request
    request = property(_getReq) 
    
    def execute(self):    
        """
        Primary override target for implementing
        domain Actions.  Default is to raise an
        Invalid Action error.
        """
        raise ActionError("Invalid Action")
       
    def directOutput(self, data, contentType="text/plain"):
        """
        Sends out data as generated by the app
        bypassing templates etc.
        """
        self.request.setResponsePage(None)        
        rsp = self.request.getResponse()
        
        rsp.outputFormat = contentType
        rsp.output = data
        
        return rsp

    def JSONOutput(self,data):
        '''
        Utility to handle JSON responses
        '''
        return self.directOutput(data, "json")

    def auth_check(self):
        """
        The default authorization check defers to the 
        session check,  If we pass the sess check, then
        the auth check is considered good.  
        
        Override this to check auth for your actions and
        if rejected simply return a non-true result code
        (AUTH_FAILED would do fine) and an explanation message.
        """
        # default delegates to session check
        return self.sess_check()
    
    def sess_check(self, createOnFail=False):
        """
        Override to enable application level
        session checking.  
        """
        rv = False
        msg = 'No Session'
        
        sess = self.request.session
        if not sess:
            if createOnFail:
                sess = self._request._domain.getSession(createOnFail=createOnFail)
                self.request.session = sess
                logging.debug("~~ Session set to: %s" % sess)

        if sess:
            rv = True
            msg = 'OK'
                             
        return rv,msg

    def setErrorResponse(self,msg):
        """
        If you have to blow up, at
        least do it nicely.
        """
        req = self.request
        req.setAttribute("results","500::%s" % msg)
        req.setAttribute("errmsg",msg)
        req.setResponsePage(req.domain.errorPage)
        
        return req.getResponse()
        
class ActionDomain(object):
    """
    Dispenser of Actions: The name parameter identifies the namespace 
    or 'logical' root for the domain it serves.  The actionRoot parameter
    identifies the path to the root file system directory used for placing 
    the target Action source file.  All parameters are passed in from the
    cfg dictionary.
    
    For example, lets set ad to an ActionDomain created with a name of 'MyDomain' 
    and the default action root ('./actions').  If a request were to come in with 
    the action set to 'user.Find'.  It would look for a class called 'Find' that 
    implements the IWebAction interface and has it's source code in the file named
    /${SERVER_ROOT}/actions/usr/find.py.  If any of these expectations are not 
    true, an ActionDomainException will be raised.
    
    The sessTO config setting will set the number of minutes that the session has 
    to sit untouched before expiration.  Default is 20 minutes.
    
    The actionCache setting controls how the ActionDomain handles caching and
    dynamic loading for Actions.  There are four settings:
    
       - None:    No caching is used.  All files are loaded on demand every time
                  which is great for development and lousy for production.  This 
                  is the default and should be changed for serious use.
                  
       - Active:  Each file is loaded and cached on first use.  This is the fasted
                  start up time with no updates loaded after the first use.
                                 
       - Preset:  All python files in actionRoot are loaded and cached at server 
                  start up.  New files can be placed in the directory and will be 
                  loaded and cached on first demand.  Good setting for testing and
                  trusted environments.
                  
       - Locked:  Like preset, but no files are loaded after start up.  This is the
                  suggested setting for production use since it is more secure than
                  allowing dynamic loading. 
    """    
    def __init__(self, cfg):
        """
        Constructor
        """
        self._name = cfg['name']
        self._acache = None
        self._cachedOnly = False
        self._aroot = cfg['actionRoot']
        self._reqcntr = 0
        self._defAction = cfg['defAction']
        self._webRoot = cfg['webRoot']
        self._htmlRoot = cfg['htmlRoot']
        self._parent = None
        self._subs = []
                
        # set up caching options
        ac = cfg['actionCache'].lower()

        if cfg.has_key("errorPage"):
            self.errorPage = cfg['errorPage']
        else:
            self.errorPage = DEFAULT_ERROR_PAGE
            
        
        if ac == "active":
            # cache on demand
            self._acache = []
        elif ac == 'locked':
            # use cached Actions only.
            # limits actions to those 
            # present at startup
            self._cachedOnly = True
            self._loadCache()
        elif ac == "preset":
            # cache what's there at
            # start up and enable 
            # load on demand for more
            self._loadCache()
        
        logging.info("~~ Action Domain %s initialized" % self._name)
        
    # -- Linkage chain --
    def _getParent(self):
        return self._parent
    def _setParent(self,p):
        self._parent = p
    parent = property(_getParent,_setParent)
       
    def addChild(self,s):
        s.parent = self
        self._subs.append(s)
        
    def getChildren(self):
        return self._subs
                
    def processRequest(self, request, rsp, sess):
        """
        Examine the request to determine the action to take.
        """
                
        # build the request
        req = Request(request, sess, self)
        
        logging.debug('~~ session: %s' % sess)
        
        # default action to REST style
        reqPath = req.path
        actionName = reqPath
                
        # manage index requests
        if reqPath[-1] == "/": 
            reqPath += "index.html" 
        
        logging.debug("~~ reqPath (p1): %s (%s)" % (reqPath,req.path))    

        # set up for response processing
        self.response = rsp
        
        # set request id for tracking...
        self._reqcntr += 1
        req.setAttribute("req_id", "%s%s" % (self._name[:3].upper(), self._reqcntr))
        
        # override w/parameter
        if 'action' in req.getParameterNames():
            actionName = req.getParameter('action')
        else:
            actionName = self._defAction
        
        logging.debug("~~ actionName (p2): %s" % actionName)    

        # declare pspy response object
        resp = None
        
        # process if found
        if reqPath.split(".")[-1].lower() in STATICS:
            logging.debug("~~ Static Content Filter (%s)" % self._name)
            resp = Response()
            resp.responsePage = reqPath

        elif actionName:
            logging.info("-- Action Request: %s " % actionName)
            # track the original action name 
            req.setAttribute('firstAction', actionName)
            
            while actionName:
                # generate class
                action = self._getActionClass(actionName.split(os.sep)[-1], req)
                logging.debug('~~ Action Class: %s' % str(action))
                    
                # authorized execution of action 
                try:
                    logging.debug('~~ Executing Action %s' % actionName)   
                    resp = action.execute()
                except Exception, ex:
                    n = Notification("!! Action Execution Failure")
                    n.setException(ex)
                    resp = Response()
                    resp.nextAction = None
                    resp.data = {"errmsg":"Action Error (see logs for details)."}
                    resp.responsePage = DEFAULT_ERROR_PAGE
                    
                # record the action name in the reponse
                resp.actionName = actionName
                
                # follow chain if requested
                if resp.nextAction:                    
                    # reuse the same request to preserve 
                    # attributes etc.
                    req = resp.request
                    
                    # avoid circulations 
                    req.chainToAction(None)
                                        
                # either way, assign it
                actionName = resp.nextAction        
                
                # remove loopy refs
                resp.request = None
        else:
            # not resolved by this domain
            raise ActionError("Invalid action requested %s" % (actionName))

        resp.request = req
        
        # generate output
        self._respond(resp)
        
    def _respond(self, resp):
        """
        Generate appropriate output
        """
        page = resp.responsePage
        
        if page:    
            ext = page.split(".")[-1]
            if ext.lower() in STATICS:
                #default cache content to known types to 2 hours
                self.response.headers['Cache-Control'] = "max-age=8600"
                mtype = CONTENT_TYPES[ext.lower()]
                self.response.headers['Content-Type'] = mtype
                if mtype.startswith('text'):
                    fmode = "r"
                else:
                    fmode = "rb"
                    
                # jic:  IF we are in Default mode we
                # we will need the page separator
                if page[0] != '/':
                    page = "/%s" % page
                    
                # send static content from html directory 
                fullpath = "%s%s" % (self._htmlRoot,page)
                logging.debug("~~ sending %s (%s)" % (fullpath,page))
                f = open(fullpath,fmode)
                self.response.out.write(f.read())
                f.close()
                
            else:
                # send dynamic data from the web directory
                fullpath = "%s%s" % (self._webRoot,page)
                logging.debug("~~ Template Location: %s" % fullpath)
                # spin out to template engine
                (pth,fn) = os.path.split(fullpath)
                
                tLoader = TemplateLoader([pth])
                tmpl = tLoader.load(fn)
                stream = tmpl.generate(data=resp.data,request=resp.request)
                
                try:
                    # generate output via genshi template
                    self.response.out.write(stream.render(method='xml'))
           
                except Exception, e:
                    Notification( "!! Error rendering stream").setException(e)
                    return None
        else:
            # try to write out response directly
            try:
                if CONTENT_TYPES.has_key(resp.outputFormat):
                    self.response.headers['Content-type'] = CONTENT_TYPES[resp.outputFormat]
                else:
                    # try that??
                    self.response.headers['Content-type'] = resp.outputFormat
                self.response.out.write(resp.output)
            except Exception,ex:
                Notification("!! Invalid Output:").setException(ex)
                
        
    def _loadCache(self, actionDir=None):
        """
        Load all python files in a directory as they are assumed to contain actions
        """
        # default to action root
        adir = actionDir or self._aroot
        for f in glob("%s%s*.py" % (adir, os.sep)):
            execfile(f)
            self._acache.append(f)
                    
    def _getActionClass(self, actionName, req):
        """
        Attempt to provide the requested action class by
        either accessing a preloaded item or by dynamically
        loading the class from a file
        """
        # strip off any parameters     
        actionName = actionName.split('?')[0]
            
        # format the path to the action module
        actionPath = actionName.replace('.', os.sep)
        fileName = "%s%s%s.py" % (self._aroot, os.sep, actionPath.lower())

        # check cache
        cached = False 
        if self._cachedOnly:
            # force built ins
            cached = True
        elif self._acache and actionName in self._acache: 
            # already cached
            cached = True

        try:
            # it may be dynamic, built-in or cached
            # if class is a file, and it is not
            # already cached, then load it up.
            if not cached and os.path.isfile(fileName):           
                # run the action script to create class
                execfile(fileName)
                if self._acache and not cached:
                    # update cache
                    self._acache.append(actionName)

            # create an instance of the alleged action
            action = eval('%s(req)' % actionName.split('.')[-1]) 
                       
        except Exception:
            # Error from python code
            raise ActionDomainError("Invalid Action Type")

        # validate as a WebAction 
        """
        if IWebAction not in implementedBy(action.__class__) :
            raise ActionDomainError("Object found is not a IWebAction")
        """
        # send it back
        return action
    
    def getName(self):
        return self._name
    name = property(getName)